/*
 * Copyright (c) 2014 Hai Bison
 *
 * See the file LICENSE at the root directory of this project for copying
 * permission.
 */

package haibison.xsettings;

import static haibison.xsettings.BuildConfig.DEBUG;

import java.security.SecureRandom;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;

/**
 * <p>
 * XSettings.
 * </p>
 * <h1>Notes</h1>
 * <p>
 * <ul>
 * <li>You use this class like a singleton class, via
 * {@link #getDefaultSettings(Context)}, {@link #getInstance(Context)}&#x2026;</li>
 * <li>This class is not thread safe.</li>
 * </ul>
 * </p>
 * 
 * @author Hai Bison
 *
 */
public class XSettings {

    private static final String CLASSNAME = XSettings.class.getName();

    /**
     * UUID of this class.
     */
    public static final String UUID = "061cff20-9cfc-4fc5-b1a1-915c19616260";

    /**
     * Library name.
     */
    public static final String LIB_NAME = "XSettings";

    /**
     * Library version code.
     */
    public static final int LIB_VERSION_CODE = 1;

    /**
     * Library version name.
     */
    public static final String LIB_VERSION_NAME = "0.0.1";

    /**
     * XSettings preferences filename.
     */
    public static final String XSETTINGS_PREFERENCES_FILENAME = LIB_NAME + "."
            + UUID;

    /**
     * The UUID.
     */
    private static final String KEY_SECURITY_UUID = "security.uuid";

    private static XSettings mDefaultSettings;
    private static XSettings mInstance;

    private static char[] mUuid;

    /**
     * Gets default settings.
     * 
     * @param context
     *            the context.
     * @return default settings.
     */
    public static XSettings getDefaultSettings(Context context) {
        if (mDefaultSettings == null)
            mDefaultSettings = new XSettings(context, null);

        return mDefaultSettings;
    }// getDefaultSettings()

    /**
     * Gets instance of XSettings.
     * 
     * @param context
     *            the context.
     * @return instance of XSettings.
     */
    public static XSettings getInstance(Context context) {
        if (mInstance == null)
            mInstance = new XSettings(context, XSETTINGS_PREFERENCES_FILENAME);

        return mInstance;
    }// getInstance()

    /**
     * Gets <strong>global unique</strong> UUID which is used by this library.
     * 
     * @param context
     *            the context.
     * @return the <strong>global unique</strong> UUID.
     */
    public static char[] getUuid(Context context) {
        if (mUuid != null)
            return mUuid;

        XSettings xSettings = getInstance(context);

        String result = xSettings.getString(KEY_SECURITY_UUID, null);
        if (TextUtils.isEmpty(result)) {
            result = SimpleWeakEncryption.Base36.toBase36(SecureRandom
                    .getSeed(Byte.MAX_VALUE));
            xSettings.setString(KEY_SECURITY_UUID, result);
        }// if

        mUuid = result.toCharArray();
        return mUuid;
    }// getUuid()

    private final Context mContext;
    private final SharedPreferences mPreferences;
    private Bundle mTransaction = null;

    /**
     * Creates new instance.
     *
     * @param context
     *            the context.
     * @param filename
     *            the filename. If {@code null} or empty, default name will be
     *            used (via
     *            {@link PreferenceManager#getDefaultSharedPreferences(Context)}
     *            ).
     */
    protected XSettings(Context context, String filename) {
        if (DEBUG)
            Log.d(CLASSNAME, "XSettings() >> " + filename);

        /*
         * Always use application context.
         */
        mContext = context.getApplicationContext();

        if (TextUtils.isEmpty(filename))
            mPreferences = PreferenceManager
                    .getDefaultSharedPreferences(mContext);
        else
            mPreferences = mContext.getSharedPreferences(filename,
                    Context.MODE_PRIVATE);
    }// XSettings()

    /**
     * Gets context.
     * 
     * @return the context.
     */
    public Context getContext() {
        return mContext;
    }// getContext()

    /**
     * Gets the underlying {@link SharedPreferences} which is used by this
     * instance.
     * 
     * @return the shared preferences.
     */
    public SharedPreferences getPreferences() {
        return mPreferences;
    }// getPreferences()

    /**
     * Gets the underlying object which manages transaction of this instance.
     * 
     * @return a bundle, can be {@code null}.
     */
    public Bundle getTransaction() {
        return mTransaction;
    }// getTransaction()

    /**
     * Convenient method for {@link #getPreferences()} and
     * {@link SharedPreferences#registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener)}
     * .
     * 
     * @param listener
     *            the listener.
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings registerOnSharedPreferenceChangeListener(
            OnSharedPreferenceChangeListener listener) {
        mPreferences.registerOnSharedPreferenceChangeListener(listener);
        return this;
    }// registerOnSharedPreferenceChangeListener()

    /**
     * Convenient method for {@link #getPreferences()} and
     * {@link SharedPreferences#unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener)}
     * .
     * 
     * @param listener
     *            the listener.
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings unregisterOnSharedPreferenceChangeListener(
            OnSharedPreferenceChangeListener listener) {
        mPreferences.unregisterOnSharedPreferenceChangeListener(listener);
        return this;
    }// unregisterOnSharedPreferenceChangeListener()

    /**
     * Begins a transaction.
     *
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings beginTransaction() {
        if (mTransaction == null)
            mTransaction = new Bundle();

        return this;
    }// beginTransaction()

    /**
     * Commits current transaction.
     *
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings commitTransaction() {
        if (mTransaction == null)
            return this;

        Editor editor = mPreferences.edit();
        String[] keys = mTransaction.keySet().toArray(new String[0]);
        for (String key : keys) {
            Object val = mTransaction.get(key);
            if (val == null)
                editor.remove(key);
            else if (val instanceof Boolean)
                editor.putBoolean(key, (Boolean) val);
            else if (val instanceof Float)
                editor.putFloat(key, (Float) val);
            else if (val instanceof Integer)
                editor.putInt(key, (Integer) val);
            else if (val instanceof Long)
                editor.putLong(key, (Long) val);
            else if (val instanceof String)
                editor.putString(key, (String) val);
        }// for
        editor.commit();

        mTransaction.clear();
        mTransaction = null;

        return this;
    }// commitTransaction()

    /**
     * Cancels current transaction.
     *
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings cancelTransaction() {
        if (mTransaction == null)
            return this;

        mTransaction.clear();
        mTransaction = null;

        return this;
    }// cancelTransaction()

    /**
     * Checks to see if this instance is using a transaction.
     *
     * @return {@code true} or {@code false}.
     */
    public boolean inTransaction() {
        return mTransaction != null;
    }// inTransaction()

    /**
     * Sets a string.
     *
     * @param key
     *            the key.
     * @param value
     *            the value.
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings setString(String key, String value) {
        if (inTransaction()) {
            mTransaction.putString(key, value);
        } else {
            Editor editor = mPreferences.edit();
            if (value == null)
                editor.remove(key);
            else
                editor.putString(key, value);
            editor.commit();
        }

        return this;
    }// setString()

    /**
     * Gets a string.
     *
     * @param key
     *            the key.
     * @return the value, or {@code null} if given key does not exist.
     */
    public String getString(String key) {
        return getString(key, null);
    }// getString()

    /**
     * Gets a string.
     *
     * @param key
     *            the key.
     * @param defaultValue
     *            default value to be returned if given key does not exist.
     * @return the value, or {@code defaultValue}.
     */
    public String getString(String key, String defaultValue) {
        if (inTransaction() && mTransaction.containsKey(key))
            return mTransaction.getString(key);

        return mPreferences.getString(key, defaultValue);
    }// getString()

    /**
     * Sets an integer.
     *
     * @param key
     *            the key.
     * @param value
     *            the value.
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings setInt(String key, int value) {
        if (inTransaction())
            mTransaction.putInt(key, value);
        else
            mPreferences.edit().putInt(key, value).commit();

        return this;
    }// setInt()

    /**
     * Gets an integer.
     *
     * @param key
     *            the key.
     * @param defaultValue
     *            default value if given key does not exist.
     * @return the value.
     */
    public int getInt(String key, int defaultValue) {
        if (inTransaction() && mTransaction.containsKey(key))
            return mTransaction.getInt(key, defaultValue);

        return mPreferences.getInt(key, defaultValue);
    }// getInt()

    /**
     * Gets an integer.
     *
     * @param key
     *            the key.
     * @return the value, or {@code 0} if given key does not exist.
     */
    public int getInt(String key) {
        return getInt(key, 0);
    }// getInt()

    /**
     * Sets a long.
     *
     * @param key
     *            the key.
     * @param value
     *            the value.
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings setLong(String key, long value) {
        if (inTransaction())
            mTransaction.putLong(key, value);
        else
            mPreferences.edit().putLong(key, value).commit();

        return this;
    }// setLong()

    /**
     * Gets a long.
     *
     * @param key
     *            the key.
     * @param defaultValue
     *            default value if given key does not exist.
     * @return the value.
     */
    public long getLong(String key, long defaultValue) {
        if (inTransaction() && mTransaction.containsKey(key))
            return mTransaction.getLong(key, defaultValue);

        return mPreferences.getLong(key, defaultValue);
    }// getLong()

    /**
     * Gets a long.
     *
     * @param key
     *            the key.
     * @return the value, or {@code 0} if given key does not exist.
     */
    public long getLong(String key) {
        return getLong(key, 0);
    }// getLong()

    /**
     * Sets a float.
     *
     * @param key
     *            the key.
     * @param value
     *            the value.
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings setFloat(String key, float value) {
        if (inTransaction())
            mTransaction.putFloat(key, value);
        else
            mPreferences.edit().putFloat(key, value).commit();

        return this;
    }// setFloat()

    /**
     * Gets a float.
     *
     * @param key
     *            the key.
     * @param defaultValue
     *            default value if given key does not exist.
     * @return the value.
     */
    public float getFloat(String key, float defaultValue) {
        if (inTransaction() && mTransaction.containsKey(key))
            return mTransaction.getFloat(key, defaultValue);

        return mPreferences.getFloat(key, defaultValue);
    }// getFloat()

    /**
     * Gets a float.
     *
     * @param key
     *            the key.
     * @return the value, or {@code 0} if given key does not exist.
     */
    public float getFloat(String key) {
        return getFloat(key, 0);
    }// getFloat()

    /**
     * Sets a boolean.
     *
     * @param key
     *            the key.
     * @param value
     *            the value.
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings setBoolean(String key, boolean value) {
        if (inTransaction())
            mTransaction.putBoolean(key, value);
        else
            mPreferences.edit().putBoolean(key, value).commit();

        return this;
    }// setBoolean()

    /**
     * Gets a boolean.
     *
     * @param key
     *            the key.
     * @param defaultValue
     *            default value if given key does not exist.
     * @return the value.
     */
    public boolean getBoolean(String key, boolean defaultValue) {
        if (inTransaction() && mTransaction.containsKey(key))
            return mTransaction.getBoolean(key, defaultValue);

        return mPreferences.getBoolean(key, defaultValue);
    }// getBoolean()

    /**
     * Gets a boolean.
     *
     * @param key
     *            the key.
     * @return the value, or {@code false} if given key does not exist.
     */
    public boolean getBoolean(String key) {
        return getBoolean(key, false);
    }// getBoolean()

    /**
     * Encrypts a string and stores it via given key.
     * 
     * @param key
     *            the key.
     * @param value
     *            the value.
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings setXString(String key, String value) {
        if (value != null)
            value = SimpleWeakEncryption.encrypt(getUuid(getContext()), null,
                    value);

        return setString(key, value);
    }// setXString()

    /**
     * Gets (decrypts) an encrypted string.
     *
     * @param key
     *            the key.
     * @return the original value, or {@code null} if given key does not exist.
     * @throws RuntimeException
     *             which wraps original exception (if any) while decrypting the
     *             value.
     */
    public String getXString(String key) {
        return getXString(key, null);
    }// getXString()

    /**
     * Gets (decrypts) an encrypted string.
     *
     * @param key
     *            the key.
     * @param defaultValue
     *            default value to be returned if given key does not exist.
     * @return the value, or {@code defaultValue}.
     * @throws RuntimeException
     *             which wraps original exception (if any) while decrypting the
     *             value.
     */
    public String getXString(String key, String defaultValue) {
        String result = getString(key);

        if (result == null)
            return defaultValue;

        return SimpleWeakEncryption
                .decrypt(getUuid(getContext()), null, result);
    }// getXString()

    /**
     * Encrypts a boolean and stores it via given key.
     * 
     * @param key
     *            the key.
     * @param value
     *            the value.
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings setXBoolean(String key, boolean value) {
        return setXString(key, Boolean.toString(value));
    }// setXBoolean()

    /**
     * Gets (decrypts) a boolean via given key.
     * 
     * @param key
     *            the key.
     * @param defaultValue
     *            default value to be returned if given key does not exist.
     * @return the value, or {@code defaultValue}.
     * @throws RuntimeException
     *             which wraps original exception (if any) while decrypting the
     *             value.
     */
    public boolean getXBoolean(String key, boolean defaultValue) {
        String s = getXString(key);

        if (s == null)
            return defaultValue;

        return Boolean.parseBoolean(s);
    }// getXBoolean()

    /**
     * Gets (decrypts) a boolean via given key.
     * 
     * @param key
     *            the key.
     * @return the value, or {@code false} if given key does not exist.
     * @throws RuntimeException
     *             which wraps original exception (if any) while decrypting the
     *             value.
     */
    public boolean getXBoolean(String key) {
        return getXBoolean(key, false);
    }// getXBoolean()

    /**
     * Encrypts an integer and stores it via given key.
     * 
     * @param key
     *            the key.
     * @param value
     *            the value.
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings setXInt(String key, int value) {
        return setXString(key, Integer.toString(value));
    }// setXInt()

    /**
     * Gets (decrypts) an integer via given key.
     * 
     * @param key
     *            the key.
     * @param defaultValue
     *            default value to be returned if given key does not exist.
     * @return the value, or {@code defaultValue}.
     * @throws RuntimeException
     *             which wraps original exception (if any) while decrypting the
     *             value.
     */
    public int getXInt(String key, int defaultValue) {
        String s = getXString(key);

        if (s == null)
            return defaultValue;

        return Integer.parseInt(s);
    }// getXInt()

    /**
     * Gets (decrypts) an integer via given key.
     * 
     * @param key
     *            the key.
     * @return the value, or {@code 0} if given key does not exist.
     * @throws RuntimeException
     *             which wraps original exception (if any) while decrypting the
     *             value.
     */
    public int getXInt(String key) {
        return getXInt(key, 0);
    }// getXInt()

    /**
     * Encrypts a long and stores it via given key.
     * 
     * @param key
     *            the key.
     * @param value
     *            the value.
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings setXLong(String key, long value) {
        return setXString(key, Long.toString(value));
    }// setXLong()

    /**
     * Gets (decrypts) a long via given key.
     * 
     * @param key
     *            the key.
     * @param defaultValue
     *            default value to be returned if given key does not exist.
     * @return the value, or {@code defaultValue}.
     * @throws RuntimeException
     *             which wraps original exception (if any) while decrypting the
     *             value.
     */
    public long getXLong(String key, long defaultValue) {
        String s = getXString(key);

        if (s == null)
            return defaultValue;

        return Long.parseLong(s);
    }// getXLong()

    /**
     * Gets (decrypts) a long via given key.
     * 
     * @param key
     *            the key.
     * @return the value, or {@code 0} if given key does not exist.
     * @throws RuntimeException
     *             which wraps original exception (if any) while decrypting the
     *             value.
     */
    public long getXLong(String key) {
        return getXLong(key, 0);
    }// getXLong()

    /**
     * Encrypts a float and stores it via given key.
     * 
     * @param key
     *            the key.
     * @param value
     *            the value.
     * @return the instance of this object, for chaining multiple calls into a
     *         single statement.
     */
    public XSettings setXFloat(String key, float value) {
        return setXString(key, Float.toString(value));
    }// setXFloat()

    /**
     * Gets (decrypts) a float via given key.
     * 
     * @param key
     *            the key.
     * @param defaultValue
     *            default value to be returned if given key does not exist.
     * @return the value, or {@code defaultValue}.
     * @throws RuntimeException
     *             which wraps original exception (if any) while decrypting the
     *             value.
     */
    public float getXFloat(String key, float defaultValue) {
        String s = getXString(key);

        if (s == null)
            return defaultValue;

        return Float.parseFloat(s);
    }// getXFloat()

    /**
     * Gets (decrypts) a float via given key.
     * 
     * @param key
     *            the key.
     * @return the value, or {@code 0} if given key does not exist.
     * @throws RuntimeException
     *             which wraps original exception (if any) while decrypting the
     *             value.
     */
    public float getXFloat(String key) {
        return getXFloat(key, 0);
    }// getXFloat()

}
